iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
ChatGPT & Codex

ChatGPT + Codex 打造高效能 AI 開發工作流系列 第 22

Day 22: LLM API 成本與效能優化:Token 計算、Caching 策略與 Model Selection 評估

  • 分享至 

  • xImage
  •  

Day 22: LLM API 成本與效能優化:Token 計算、Caching 策略與 Model Selection 評估 (Token Cost Caching Model Selection)

本日核心價值 (Core Focus): 用可重現的 Token 計量、確定性 Prompt 快取、模型分級與 Context 截斷,把 LLM 工作流的單次成本與延遲變成可觀測、可預算的工程指標,而不是月底才看帳單。

概念說明與實戰情境 (Overview)

接上 Day 21 的防禦層之後,每次請求都可能多帶系統政策、分隔符與 Schema,Context 會膨脹。成本通常不是「模型很貴」單一原因,而是四件事疊加:重複送同一段長 system prompt、用旗艦模型做分類、把整份文件塞進視窗、以及沒有依請求記帳。優化順序應是:先量 Token,再把不變的前綴快取,再用便宜模型做分流,最後才對長文件做排序截斷。效能(延遲)與成本往往同向:較短的輸入、較小的模型、可批次的工作,三者同時改善帳單與 p95。

關鍵操作與範例 (Implementation & Example)

1. 先量 Token,再談省錢

OpenAI 相容模型常用 tiktoken。沒裝套件時,可用粗估:英文約 4 字元 1 token,繁中常接近 1–2 字 1 token,程式碼介於兩者之間。粗估只供開發期告警,上線計費仍以 API 回傳的 usage 為準。

from __future__ import annotations

import hashlib
import json
import time
from dataclasses import dataclass, asdict

try:
    import tiktoken

    _ENC = tiktoken.get_encoding("o200k_base")
except Exception:  # pragma: no cover - fallback when tiktoken missing
    _ENC = None


def count_tokens(text: str) -> int:
    if _ENC is not None:
        return len(_ENC.encode(text or ""))
    # Approximate: CJK closer to 1 token/char; ASCII ~4 chars/token.
    if not text:
        return 0
    cjk = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff")
    other = len(text) - cjk
    return cjk + max(1, other // 4)


@dataclass
class RequestCostLog:
    request_id: str
    model: str
    input_tokens: int
    output_tokens: int
    cached_input_tokens: int
    latency_ms: int
    usd_estimate: float
    cache_hit: bool


# Replace with your team's current price sheet; numbers are placeholders.
PRICE_PER_1M = {
    "gpt-4.1-mini": {"input": 0.40, "cached_input": 0.10, "output": 1.60},
    "gpt-4.1": {"input": 2.00, "cached_input": 0.50, "output": 8.00},
}


def estimate_usd(model: str, input_tokens: int, output_tokens: int, cached: int = 0) -> float:
    p = PRICE_PER_1M[model]
    billed_input = max(0, input_tokens - cached)
    return (
        billed_input * p["input"]
        + cached * p["cached_input"]
        + output_tokens * p["output"]
    ) / 1_000_000


_PROMPT_CACHE: dict[str, str] = {}


def cache_key(model: str, messages: list[dict[str, str]]) -> str:
    blob = json.dumps({"model": model, "messages": messages}, ensure_ascii=False, sort_keys=True)
    return hashlib.sha256(blob.encode("utf-8")).hexdigest()


def complete_with_local_cache(model: str, messages: list[dict[str, str]], call_api) -> tuple[str, RequestCostLog]:
    """Cache bit-identical prompts (classification, routing, lint-style checks)."""
    key = cache_key(model, messages)
    t0 = time.perf_counter()
    if key in _PROMPT_CACHE:
        text = _PROMPT_CACHE[key]
        cached = True
        in_tok = count_tokens("".join(m["content"] for m in messages))
        out_tok = count_tokens(text)
        usd = 0.0
    else:
        text = call_api(model, messages)
        _PROMPT_CACHE[key] = text
        cached = False
        in_tok = count_tokens("".join(m["content"] for m in messages))
        out_tok = count_tokens(text)
        usd = estimate_usd(model, in_tok, out_tok, cached=0)
    log = RequestCostLog(
        request_id=key[:12],
        model=model,
        input_tokens=in_tok,
        output_tokens=out_tok,
        cached_input_tokens=in_tok if cached else 0,
        latency_ms=int((time.perf_counter() - t0) * 1000),
        usd_estimate=usd,
        cache_hit=cached,
    )
    print(json.dumps(asdict(log), ensure_ascii=False))
    return text, log

2. 兩層 Caching:應用層確定性快取,與供應商 Prompt Caching

應用層快取適合輸入完全相同的工作:意圖分類、語言偵測、固定 Schema 的 lint、同一份 OpenAPI 的摘要。Key 必須包含 model、訊息本體與溫度;temperature=0 才值得快取。不要快取含不可信 HTML 的原始請求(Day 21):應快取「抽完純文字 + 截斷後」的正規化結果。

供應商 Prompt Caching 是另一層:把長且穩定的前綴(系統政策、工具定義、Schema)放在訊息最前面,變動的使用者問題與檢索片段放最後。前綴重複出現時,供應商可能對 cached input tokens 計較低單價。實務要點是「前綴位元組級穩定」:多一個空白、隨機 request id 塞進 system,快取就斷。不要把時間戳、trace id 寫進 system;那些放 header 或最後一則 user message。

3. Model Selection:分類走便宜模型,寫程式走強模型

工作 建議模型級距 原因
意圖分類、路由、語言偵測、是否需要 RAG 便宜 / mini 高流量、輸出短、錯了可用規則或二次確認
每日摘要、郵件草稿、JSON 抽取 中階 要穩的 Structured Output,但不需深推理
產生或修改程式、複雜 Debug、架構取捨 旗艦 / 強模型 需要較長推理與較低幻覺成本
大量離線標註、回填 embedding 前的清理 Batch API + 便宜模型 可等數分鐘到數小時,單價較低

分流本身也可模型化:先用 mini 輸出 { "task": "classify|codegen|rag" },再把原請求轉給對應模型。分類 Prompt 保持確定性並走應用層快取,這條路徑的邊際成本會接近零。

4. 截斷 Context:先排序,再切長度

Token 上限不是「從尾巴砍」。對 RAG 或長 log,先用檢索分數或規則權重排序,保留 top-k 與必要 citation,其餘丟棄。Day 21 的 MAX_UNTRUSTED_CHARS 同時是安全與成本控制。實作時分別預算:system 政策固定 N token、檢索 M token、使用者問題 P token,超標只砍檢索,不砍政策。

5. Batch 與每請求記帳

非同步工作(夜間產生 Release Notes、回填分類、大量文件摘要)應走 Batch,而不是在 HTTP 請求裡同步打旗艦模型。無論同步或 Batch,都要把 usage.input_tokensusage.output_tokens、cached tokens、model、latency 寫進結構化 log。沒有 per-request 成本,Model Selection 表只是感覺,無法做回歸。建議每筆至少含:request_idmodeltask(classify / codegen / rag)、input_tokensoutput_tokenscached_input_tokensusd_estimatecache_hitlatency_ms。用這些欄位才能回答「分類是否該繼續用 mini」而不是「這個月模型很貴」。價格表會改,PRICE_PER_1M 應當設定檔,不要寫死在多處常數。同一個分類 Prompt 若每天打一萬次且輸入位元組級相同,應用層快取的效益會大於換更便宜的模型;先看 log 裡的 cache_hit 比例,再決定要不要改價目表。

注意事項與常見失敗 (Pitfalls)

  • 用字數當 Token。修法:開發用 tiktoken 或粗估告警;帳單以 API usage 為準,兩者都寫進 log。
  • 把 trace id、時間、隨機 nonce 放進 system prompt,導致 Prompt Caching 永遠 miss。修法:system 前綴凍結;變動欄位放最後一個 user message。
  • 全站只用一個旗艦模型。修法:分類 / 路由 mini,codegen 強模型;用表格定期重估價目與品質。
  • 快取未含 model 與訊息正規化,或快取了含 PII 的原始 HTML。修法:key 含 model;只快取截斷後的純文字;TTL 與租戶隔離。
  • 為了省 Token 從文件開頭硬切,把最相關段落切掉。修法:先 rank 再截斷;低分片段直接不送(接 Day 23 的 score threshold)。
  • 優化了單價卻沒記每次請求。修法:每請求一筆 JSON log,才能算「每次成功任務的美元」而不是「這個月模型很貴」。

本日總結 (Takeaways)

  • 先量 Token(tiktoken 或粗估 + API usage),沒有計量就沒有優化。
  • 確定性 Prompt 走應用層快取;長前綴靠供應商 Prompt Caching,前綴必須穩定。
  • 便宜模型做分類與路由,強模型做程式與高風險生成;離線工作用 Batch。
  • Context 用排序後的 top-k 截斷,並記錄每請求成本、模型與是否命中快取。

明日預告 (Next)

截斷與 top-k 已經是檢索問題的一半。下一步把知識從「整份貼進 Prompt」改成可引用的知識庫:Day 23 將構建專屬知識庫工作流 (RAG Architecture):結合 Vector Database 與 Context Retrieval。


上一篇
Day 21: 資安防護工作流:Prompt Injection 攻擊範例與 LLM 防禦策略
下一篇
Day 23: 構建專屬知識庫工作流 (RAG Architecture):結合 Vector Database 與 Context Retrieval
系列文
ChatGPT + Codex 打造高效能 AI 開發工作流24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言